package org.flexthinker.binding.form.support
{
    import flash.events.EventDispatcher;

    import org.flexthinker.binding.form.ConfigurableFormModel;
    import org.flexthinker.binding.value.CommitTrigger;
    import org.flexthinker.binding.value.ValueModel;
    import org.flexthinker.binding.value.support.BufferedValueModel;
    import org.flexthinker.binding.value.support.ValueHolder;

    import flash.utils.Dictionary;

    import mx.utils.StringUtil;

    /**
     *  Abstract class for <code>FormModel</code>s.
     *
     *  @see org.flexthinker.binding.form.ConfigurableFormModel
     *  @see org.flexthinker.binding.form.FormModel
     */
    public class AbstractFormModel extends EventDispatcher implements ConfigurableFormModel
    {
        private var _id:String;
        private var buffered:Boolean = false;
        private var domainObjectHolder:ValueModel;

        private var domainObjectWrapper:Object;

        private var propertyValueModels:Dictionary;

        protected var commitTrigger:CommitTrigger = new CommitTrigger();

        private var fieldNames:Vector.<String> = new Vector.<String>();


        public function AbstractFormModel(domainObject:Object, buffered:Boolean = false)
        {
            domainObjectWrapper = {value:domainObject};
            init(new ValueHolder(domainObjectWrapper, "value"), buffered)
        }

        private function init(domainObjectHolder:ValueModel, buffered:Boolean = false):void
        {
            this.domainObjectHolder = domainObjectHolder;
            this.buffered = buffered;
            propertyValueModels = new Dictionary();
        }

        public function getFormObject():Object
        {
            return domainObjectHolder.getValue();
        }


        public function setFormObject(value:Object):void
        {
            domainObjectHolder.setValue(value);
            //setEnabled(true);
            revert();
        }


        public function getId():String
        {
            return _id;
        }


        public function setId(id:String):void
        {
            _id = id;
        }

        public function add(propertyName:String):ValueModel
        {
            return addValueModelFor(propertyName, createValueModel(propertyName));
        }


        public function addValueModelFor(propertyName:String, valueModel:ValueModel):ValueModel
        {
            if (getFormObject() == null)
            {
                throw new Error("Can't add property as long as form's object is null. Use setFormObject to set a non-null form object");
            }

            if (propertyValueModels[ propertyName ] != null)
            {
                throw new Error(StringUtil.substitute("Property {0} has been already added", propertyName));
            }
            if (! getFormObject().hasOwnProperty(propertyName))
            {
                throw new Error(StringUtil.substitute("Property {0} doesn't belong to the form Object {1}",
                        propertyName,
                        getFormObject()));
            }

            if (valueModel is BufferedValueModel)
            {
                BufferedValueModel(valueModel).setCommitTrigger(commitTrigger);
            }

            valueModel = preProcessNewValueModel(propertyName, valueModel);
            propertyValueModels[ propertyName ] = valueModel;
            fieldNames.push(propertyName);
            postProcessNewValueModel(propertyName, valueModel);
            return valueModel;
        }


        public function getValueModel(formProperty:String):ValueModel
        {
            var vm:ValueModel = propertyValueModels[formProperty] as ValueModel;
            if (vm == null)
            {
                vm = add(formProperty);
            }
            return vm;
        }


        public function hasValueModel(formProperty:String):Boolean
        {
            return ( (propertyValueModels[formProperty] as ValueModel ) != null );
        }


        public function getFieldNames():Vector.<String>
        {
            return fieldNames;
        }

        /**
         * Calling this method will cause all buffered value models
         * to revert to the original values contained by the <code>formObject</code>
         */
        public function revert():void
        {
            commitTrigger.revert(getFormObject());
        }


        public function commit():void
        {
            if (getFormObject() == null)
            {
                return;
            }

            if (!isCommittable())
            {
                throw new Error("FormModel is not commitable.")
            }

            preCommit();

            if ( !isCommittable())
            {
                throw new Error("FormModel became non-commitable after preCommit phase");
            }

            // this will cause all buffered value models to commit the buffered values
            commitTrigger.commit();

            postCommit();
        }


        public function isBuffered():Boolean
        {
            return buffered;
        }


        public function isCommittable():Boolean
        {
            // this method should be implemented in the super class
            return true;
        }


        public function isDirty():Boolean
        {
            return false; // dirtyValueAndFormModels.length > 0;;
        }

        // --------------------
        // internal methods
        // --------------------
        protected function createValueModel(formProperty:String):ValueModel
        {
            return buffered ? new BufferedValueModel(createPropertyValueModel(formProperty))
                    : createPropertyValueModel(formProperty);
        }

        protected function createPropertyValueModel(formProperty:String):ValueModel
        {
            return new ValueHolder(getFormObject(), formProperty);
        }

        /**
         * Provides a hook for subclasses to optionally decorate a new value model
         * added to this form model.
         */
        protected function preProcessNewValueModel(formProperty:String, valueModel:ValueModel):ValueModel
        {
            return valueModel;
        }

        /**
         * Provides a hook for subclasses to perform some processing after a new
         * value model has been added to this form model.
         */
        protected function postProcessNewValueModel(formProperty:String, valueModel:ValueModel):void
        {}

        /**
         * Hook for subclasses to intercept before a commit.
         */
        protected function preCommit():void
        {
        }

        /**
         * Hook for subclasses to intercept after a successful commit has finished.
         */
        protected function postCommit():void
        {
        }

    }
}